package com.lemon.exam.service.impl;

import com.alibaba.fastjson2.JSON;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.ConfigProperties;
import com.lemon.exam.common.util.PaperUtil;
import com.lemon.exam.common.web.Result;
import com.lemon.exam.entity.dto.GradingPaperAnswerDTO;
import com.lemon.exam.entity.dto.PaperExcelDTO;
import com.lemon.exam.entity.dto.PaperGradingDTO;
import com.lemon.exam.entity.dto.UserScoreDTO;
import com.lemon.exam.entity.po.PaperAnswerPO;
import com.lemon.exam.entity.po.PaperPO;
import com.lemon.exam.entity.vo.PaperGradingVO;
import com.lemon.exam.entity.vo.PaperQuestionVO;
import com.lemon.exam.entity.vo.PaperStatisticsVO;
import com.lemon.exam.entity.vo.PaperVO;
import com.lemon.exam.service.IGradingPaperService;
import com.lemon.exam.service.IPaperAnswerService;
import com.lemon.exam.service.IPaperService;
import com.lemon.exam.service.IUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;

import static com.lemon.exam.common.enums.CodeEnum.ERROR_402;

/**
 * 阅卷 服务实现类
 *
 * @author Lemon
 * @since 2025/5/17 11:23
 */
@Slf4j
@Service
public class GradingPaperServiceImpl implements IGradingPaperService {
    @Resource
    private IPaperService paperService;
    @Resource
    private IPaperAnswerService paperAnswerService;
    @Resource
    private IUserService userService;
    @Resource
    private ConfigProperties configProperties;
    @Resource
    private DatabaseClient databaseClient;

    @Override
    public Flux<PaperVO> findPaperList(String type, int pageNum, int pageSize) {
        return switch (type) {
            case "padding" -> paperService.findPaddingPaperByGradingUser(pageNum, pageSize);
            case "finish" -> paperService.findFinishPaperByGradingUser(pageNum, pageSize);
            default -> throw new CustomException(ERROR_402);
        };
    }

    @Override
    public Mono<Result<PaperGradingVO>> grading(Long paperId, Long answerId) {
        return paperService.findPaperById(paperId).flatMap(paper -> {
            //交卷10分钟后方可阅卷
            if (LocalDateTime.now().isBefore(paper.getEndTime().plusMinutes(10))) {
                return Mono.just(Result.err(402, "交卷10分钟后方可阅卷！"));
            }
            return Mono.zip(
                    PaperUtil.getPaperQuestion(configProperties.getUploadPath(paper.getFilePath())).subscribeOn(Schedulers.boundedElastic()),
                    paperAnswerService.findAnswerByPaperId(paper.getId(), answerId).subscribeOn(Schedulers.boundedElastic()),
                    paperAnswerService.hasNextPaperAnswerByPaperId(paper.getId()).subscribeOn(Schedulers.boundedElastic())
            ).flatMap(tuple -> toGradingPaper(tuple.getT1(), tuple.getT2(), tuple.getT3(), paper));
        });
    }

    /**
     * 转换阅卷数据
     *
     * @param paperQuestionList
     * @param paperAnswer
     * @param hasNext
     * @param paper
     * @return
     */
    private Mono<Result<PaperGradingVO>> toGradingPaper(List<PaperQuestionVO> paperQuestionList, PaperAnswerPO paperAnswer, Boolean hasNext, PaperPO paper) {
        return Mono.fromCallable(() -> {
            //1.用户提交的答案
            Map<Integer, String[]> answerMap = paperAnswer.getAnswerMap();
            if (!CollectionUtils.isEmpty(answerMap)) {
                paperQuestionList.stream()
                        .filter(question -> answerMap.containsKey(question.getRowIndex()))
                        .forEach(question -> question.setSubmitAnswer(answerMap.get(question.getRowIndex())));
            }
            //2.用户得分
            Map<Integer, Double> questionScoreMap = paperAnswer.getQuestionScoreMap();
            if (!CollectionUtils.isEmpty(questionScoreMap)) {
                paperQuestionList.stream()
                        .filter(question -> questionScoreMap.containsKey(question.getRowIndex()))
                        .forEach(question -> question.setUserScore(questionScoreMap.get(question.getRowIndex())));
            }
            return Result.ok(new PaperGradingVO(
                    paperAnswer.getId(),
                    hasNext,
                    paperAnswer.getTotalScore(),
                    paper.toPaperVO(),
                    paperQuestionList
            ));
        });
    }

    @Override
    public Mono<Map<String, Object>> getGradingPaper(Long paperId) {
        return paperService.findPaperById(paperId)
                .flatMap(paper -> Mono.zip(
                                userService.findUserIdAndUserNameByClassId(paper.getClassId()).subscribeOn(Schedulers.boundedElastic()),
                                paperAnswerService.findAnswerListByPaperId(paper.getId()).subscribeOn(Schedulers.boundedElastic())
                        )
                        .flatMap(tuple -> toAnswerMap(tuple.getT1(), tuple.getT2(), paper)));
    }

    /**
     * 转换答案
     * 状态[1未阅卷，2已通过，3未通过，4未考试]
     *
     * @param userMap
     * @param answers
     * @param paper
     * @return
     */
    private Mono<Map<String, Object>> toAnswerMap(Map<Long, String> userMap, List<PaperAnswerPO> answers, PaperPO paper) {
        return Mono.fromCallable(() -> {
            //1.未阅卷的集合
            List<GradingPaperAnswerDTO> noReadAnswerList = new LinkedList<>();
            //2.除了未阅卷以外的数据集合
            List<GradingPaperAnswerDTO> otherAnswerList = new LinkedList<>();
            //3.所有用户的ID
            Set<Long> userIdSet = new HashSet<>();
            for (PaperAnswerPO answer : answers) {
                userIdSet.add(answer.getUserId());
                if (answer.getPassed() == null) {
                    noReadAnswerList.add(new GradingPaperAnswerDTO(answer.getId(), userMap.get(answer.getUserId()), answer.getSubmitTime(), 1, null));
                } else {
                    otherAnswerList.add(new GradingPaperAnswerDTO(answer.getId(), userMap.get(answer.getUserId()), answer.getSubmitTime(), answer.getPassed() == 1 ? 2 : 3, answer.getTotalScore()));
                }
            }
            //4.未考试的人
            List<String> noExamUserList = userMap.entrySet().stream().filter(entry -> !userIdSet.contains(entry.getKey())).map(Map.Entry::getValue).toList();
            if (!CollectionUtils.isEmpty(noExamUserList)) {
                otherAnswerList.addAll(noExamUserList.stream().map(userName -> new GradingPaperAnswerDTO(userName, 4)).toList());
            }
            if (!CollectionUtils.isEmpty(otherAnswerList)) {
                noReadAnswerList.addAll(otherAnswerList);
            }
            return Map.of("paper", paper.toPaperVO(), "answers", noReadAnswerList);
        });
    }

    @Override
    public Mono<String> scoring(PaperGradingDTO param) {
        return paperService.findPassScoreFilePathByAnswerId(param.getAnswerId())
                .flatMap(paper -> PaperUtil.getPaperExcel(configProperties.getUploadPath(paper.getFilePath()))
                        .flatMap(excel -> validateAndScore(paper, param.getQuestionScore(), excel, param.getAnswerId()))
                        .onErrorResume(e -> {
                            log.error("评分过程发生错误，答卷ID: {}", param.getAnswerId(), e);
                            return Mono.error(new CustomException("评分失败: " + e.getMessage()));
                        }));
    }

    /**
     * 验证并评分
     *
     * @param paper
     * @param questionScore
     * @param excel
     * @return
     */
    private Mono<String> validateAndScore(PaperPO paper, Map<Integer, Double> questionScore, List<PaperExcelDTO> excel, Long answerId) {
        //1.验证数据完整性
        if (excel.size() != questionScore.size()) {
            String errorMsg = "未完全阅卷！试卷题目数: %d, 已评分题目数: %d".formatted(excel.size(), questionScore.size());
            log.warn(errorMsg);
            return Mono.error(new CustomException(errorMsg));
        }

        //2.验证分数范围
        List<String> validationErrors = excel.parallelStream()
                .map(dto -> validateQuestionScore(dto, questionScore.get(dto.getRowIndex())))
                .filter(Objects::nonNull)
                .toList();

        if (!CollectionUtils.isEmpty(validationErrors)) {
            String errorMsg = String.join("; ", validationErrors);
            log.warn("阅卷分数验证失败: {}", errorMsg);
            return Mono.error(new CustomException(errorMsg));
        }

        //3.计算总分
        double sum = questionScore.values().parallelStream()
                .filter(Objects::nonNull)
                .mapToDouble(Double::doubleValue)
                .sum();
        log.info("答卷ID: {}, 总分: {}, 及格分数: {}, 考试总分: {}", answerId, paper.getTotalScore(), paper.getPassScore(), sum);

        int passStatus = sum >= paper.getPassScore() ? 1 : 0;

        //4.更新成绩
        return paperAnswerService.update(answerId, JSON.toJSONString(questionScore), sum, passStatus)
                .doOnSuccess(result -> log.info("答卷ID: {} 评分完成，总分: {}", answerId, sum))
                .doOnError(error -> log.error("更新答卷成绩失败，答卷ID: {}", answerId, error));
    }

    @Override
    public Mono<PaperStatisticsVO> statistics(Long paperId) {
        return Mono.zip(
                paperService.findPaperById(paperId).subscribeOn(Schedulers.boundedElastic()),
                this.findUserScore(paperId).collectList()
        ).flatMap(tuple -> Mono.zip(
                this.getSegmentation(tuple.getT2(), tuple.getT1().getTotalScore()),
                this.getPassRate(tuple.getT2(), tuple.getT1().getPassScore()),
                this.getAverageScore(tuple.getT2())
        ).map(t -> new PaperStatisticsVO(tuple.getT1().toPaperVO(), tuple.getT2(), t.getT1(), t.getT2(), t.getT3())));
    }

    /**
     * 验证题目分数
     *
     * @param dto
     * @param score
     * @return
     */
    private String validateQuestionScore(PaperExcelDTO dto, Double score) {
        if (score == null) {
            return "第%d题未评分".formatted(dto.getRowIndex() - 1);
        }

        double maxScore = Double.parseDouble(dto.getScore());
        if (score < 0 || score > maxScore) {
            return "第%d题分数不正确(得分:%.1f, 满分:%.1f)".formatted(dto.getRowIndex() - 1, score, maxScore);
        }
        return null;
    }

    /**
     * 根据试卷ID查询所有用户得分
     *
     * @param paperId
     * @return
     */
    private Flux<UserScoreDTO> findUserScore(Long paperId) {
        return databaseClient.sql("""
                        SELECT
                            u.`name`,
                            pa.total_score
                        FROM
                            biz_paper_answer pa
                            LEFT JOIN sys_user u ON u.id = pa.user_id
                        WHERE
                            pa.paper_id = :paperId
                        ORDER BY pa.total_score DESC
                        """)
                .bind("paperId", paperId)
                .map(row -> new UserScoreDTO(row.get("name", String.class), row.get("total_score", Double.class)))
                .all()
                .subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 计算分段得分
     *
     * @param userScoreList
     * @param totalScore
     * @return
     */
    private Mono<Map<String, Long>> getSegmentation(List<UserScoreDTO> userScoreList, Double totalScore) {
        return Mono.fromCallable(() -> {
            //1.校验参数
            if (CollectionUtils.isEmpty(userScoreList) || totalScore == null || totalScore <= 0) {
                return Collections.<String, Long>emptyMap();
            }

            //2.计算分段边界
            int segment = (int) Math.floor(totalScore / 5);
            int[] boundaries = new int[5];
            for (int i = 0; i < 5; i++) {
                boundaries[i] = segment * i;
            }

            //3.分数集合
            List<Double> scores = userScoreList.parallelStream()
                    .map(UserScoreDTO::getScore)
                    .filter(Objects::nonNull)
                    .toList();

            //4.计算分段得分
            Map<String, Long> ret = new LinkedHashMap<>(5);
            ret.put("%s分以下".formatted(boundaries[1]), scores.parallelStream().mapToDouble(Double::doubleValue).filter(score -> score < boundaries[1]).count());
            ret.put("%s~%s分".formatted(boundaries[1], boundaries[2]), scores.parallelStream().mapToDouble(Double::doubleValue).filter(score -> score >= boundaries[1] && score < boundaries[2]).count());
            ret.put("%s~%s分".formatted(boundaries[2], boundaries[3]), scores.parallelStream().mapToDouble(Double::doubleValue).filter(score -> score >= boundaries[2] && score < boundaries[3]).count());
            ret.put("%s~%s分".formatted(boundaries[3], boundaries[4]), scores.parallelStream().mapToDouble(Double::doubleValue).filter(score -> score >= boundaries[3] && score < boundaries[4]).count());
            ret.put("%s分以上".formatted(boundaries[4]), scores.parallelStream().mapToDouble(Double::doubleValue).filter(score -> score >= boundaries[4]).count());
            return ret;
        }).subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 计算及格率
     *
     * @param userScoreList
     * @param passScore
     * @return
     */
    private Mono<Double> getPassRate(List<UserScoreDTO> userScoreList, Double passScore) {
        return Mono.fromCallable(() -> {
                    long passCount = userScoreList.parallelStream()
                            .filter(userScore -> userScore.getScore() >= passScore)
                            .count();

                    double rate = passCount * 100.0 / userScoreList.size();

                    //四舍五入保留1位小数
                    return BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP).doubleValue();
                })
                .subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 计算平均分
     *
     * @param userScoreList
     * @return
     */
    private Mono<Double> getAverageScore(List<UserScoreDTO> userScoreList) {
        return Mono.fromCallable(() -> {
                    double avg = userScoreList.parallelStream()
                            .mapToDouble(UserScoreDTO::getScore)
                            .average()
                            .orElse(0.0);
                    //四舍五入保留两位小数
                    return BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP).doubleValue();
                })
                .subscribeOn(Schedulers.boundedElastic());
    }
}
